package com.openswimsoftware.orm.model;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;

import com.openswimsoftware.orm.ORM;
import com.openswimsoftware.orm.model.annotation.Child;
import com.openswimsoftware.orm.model.annotation.Children;
import com.openswimsoftware.orm.model.annotation.Ignore;
import com.openswimsoftware.orm.model.annotation.Parent;
import com.openswimsoftware.orm.model.exception.InvalidModelField;
import com.openswimsoftware.orm.model.exception.InvalidModelRelationship;
import com.openswimsoftware.orm.model.list.ModelArrayList;
import com.openswimsoftware.orm.model.list.ModelList;
import com.openswimsoftware.orm.model.query.ModelDelete;
import com.openswimsoftware.orm.model.query.ModelFind;
import com.openswimsoftware.orm.model.query.ModelSave;
import com.openswimsoftware.orm.model.query.where.WhereOperator;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.util.Log;

@SuppressWarnings("unchecked")
public class Model<E extends Model<E>> {
	
	@Ignore
	private boolean loaded = false;
	@Ignore
	protected boolean updated = false;
	@Ignore
	private Class<E> modelType;
	@Ignore
	protected Context context;
	@Ignore
	private String parentName;
	@Ignore
	private long parentId = -1;
	
	
	private long id = -1;

	public Model(Context context, Class<E> modelType) 
	{
		this.context = context;
		this.modelType = modelType;
	}

	public Model(Context context, Class<E> modelType, long id) 
	{
		this(context, modelType);
		
		this.id = id;
	}
	
	public void setId(long id) 
	{
		this.updated = true;
		
		this.id = id;
	}
	
	public long getId() 
	{
		if (this.id == -1 && this.parentId > -1)
		{
			try 
			{
				this.findQuery().where(this.getParentName(), WhereOperator.EQUAL, this.getParentId()+"").execute();
			} 
			catch (InvalidModelField e) 
			{
				e.printStackTrace();
			}
		}
		
		return this.id;
	}
	
	public void setParentId(long parentId) 
	{
		this.parentId = parentId;
	}
	
	public long getParentId() 
	{
		return this.parentId;
	}
	
	public void setParentName(String name) 
	{
		this.parentName = name;
	}
	
	public String getParentName() 
	{
		return this.parentName;
	}
	
	public void load()
	{
		if ((this.loaded == false && this.id > -1) || (this.loaded == false && this.parentId > -1))
		{
			this.find();
		}
	}
	
	public boolean getUpdated()
	{
		return this.updated;
	}
	
	public E setUpdated(boolean updated)
	{
		this.updated = updated;
		
		return (E) this;
	}
	
	public boolean getLoaded()
	{
		return this.loaded;
	}
	
	public void setLoaded(boolean loaded)
	{
		this.loaded = loaded;
	}
	
	public E find()
	{
		return this.findQuery().execute();
	}
	
	public ModelFind<E> findQuery()
	{
		return new ModelFind<E>(context, modelType, (E) this);
	}

	public E save()
	{
		if (this.getUpdated() == true)
		{
			return this.saveQuery().execute();
		}
		else
		{
			return (E) this;
		}
	}
	
	public ModelSave<E> saveQuery()
	{
		return new ModelSave<E>(context, modelType, (E) this);
	}
	
	public boolean delete()
	{
		return this.deleteQuery().execute();
	}
	
	public ModelDelete<E> deleteQuery()
	{
		return new ModelDelete<E>(context, modelType, (E) this);
	}
	
	public ContentValues toContent() throws IllegalArgumentException, IllegalAccessException
	{
		ContentValues values = new ContentValues();
		
		Log.d("orm", "Converting model to content");
		
		for (Field field: ORM.getDataFields(modelType))
		{
			Log.d("orm", "Getting value for data field " + field.getName());
			if ( ! field.getName().equals("id"))
			{
				values = ORM.putFieldData(values, field, this);
			}
		}
		
		for (Field field: ORM.getRelationshipFields(modelType))
		{
			field.setAccessible(true);
			if (field.get(this) != null)
			{
				if (field.isAnnotationPresent(Parent.class))
				{	
					long relationshipId = ((Model<?>) field.get(this)).getId();
							
					values.put(ORM.fieldName(field), relationshipId);
				}
				else if (field.isAnnotationPresent(Child.class))
				{
					((Model<?>) field.get(this)).save();
				}
				else if (field.isAnnotationPresent(Children.class))
				{
					((ModelList<?>) field.get(this)).save();
				}
			}
		}
		
		return values;
	}
	
	@SuppressWarnings("rawtypes")
	public E fromCursor(Cursor cursor) throws IllegalArgumentException, IllegalAccessException, NoSuchMethodException, InstantiationException, InvocationTargetException, InvalidModelRelationship
	{
		for (Field field: ORM.getDataFields(modelType))
		{
			field.set(((E) this), ORM.getFieldData(cursor, field, this));
		}
		
		for (Field field: ORM.getRelationshipFields(modelType))
		{
			Log.d("orm", "Adding relationship on field " + field.getName());
			field.setAccessible(true);
			Class<?> relationshipType = field.getType();
			Log.d("orm", "Relationship is type " + relationshipType.getName());
			
			if (field.isAnnotationPresent(Parent.class))
			{
				long relationshipId = cursor.getLong(cursor.getColumnIndex(ORM.fieldName(field)));
				
				Constructor<?> constructor;
				try 
				{
					constructor = relationshipType.getConstructor(Context.class, Long.TYPE);
				} 
				catch (NoSuchMethodException e) 
				{
					constructor = relationshipType.getConstructor(Context.class, Long.class);
				}
				constructor.setAccessible(true);
				
				field.set(this, constructor.newInstance(this.context, relationshipId));
			}
			else if (field.isAnnotationPresent(Child.class))
			{	
				Constructor<?> constructor = relationshipType.getConstructor(Context.class);
				constructor.setAccessible(true);
				
				Model<?> child = (Model<?>) constructor.newInstance(this.context);
				child.setParentName(ORM.findParent(relationshipType).getName());
				child.setParentId(this.getId());
				
				field.set(this, child);
			}
			else if (field.isAnnotationPresent(Children.class))
			{
				Class<?> childType = field.getAnnotation(Children.class).modelType();
				
				ModelList<?> childList = new ModelArrayList(this.context, childType);
				childList.setParentName(ORM.findParent(childType).getName());
				childList.setParentId(this.getId());
				
				field.set(this, childList);
			}
		}
		
		return (E) this;
	}
	
}
